iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
1

很久很久以前,我還在寫Android的時候,要做異步處理有很多選擇。從早期的AsyncTask,中期的Rx,到後來的Coroutine,不只是異步處理的語法越來越直覺,多執行緒的切換也更加容易。於是開始學習Flutter之後,遇到了Dart async/await/Future,不知為何就擅自認定了,它們同樣是可以簡單地把工作丟到其它執行緒處理的語法,直到經過了一段長到我不願意承認的時間後,我才發現事情並沒有那麼單純。

如果你也有跟我一樣的幻覺,現在是時候醒過來了。異步處理不等於平行處理。當我們呼叫async函數,使用Future時,其實只是在告訴Dart「嘿,這段程式碼不急,你有空再幫我處理就好了。」於是Dart會把這份工作加進某個人的待辦清單裡面,而實際上你指派的所有的工作都是同一個人在處理。在Dart裡面,這個人叫做Isolate,而那份待辦清單叫做Event Loop。

Isolate是什麼?

基本上,Isolate就是Dart程式的Process,多個Isolate之間不會共享記憶體,而是靠互相發送訊息來溝通。和一般Process不同的是,每一個Isolate只會有單一執行緒,而我們在這個執行緒上使用所有的asyn/await/Future,最後都是同樣在這個執行緒上,根據Event Loop機制執行的。

當我們的Dart程式從main()開始執行時,就會啟動一個叫做main的Isolate。如果你還有印象,我們其實在之前的文章也曾經看過它:
https://ithelp.ithome.com.tw/upload/images/20200915/20129053Qo31fKYqvZ.png

一般來說,我們寫的絕大多數Flutter程式碼都是在這個main Isolate裡執行的。如果我們有一些比較繁重的工作,想要實現真正的平行處理的話,我們就必須啟動一個新的Isolate。

如何使用Isolate?

之前我們提到Isolate之間是靠message來溝通的,然而實現Isolate雙向溝通的Dart API有點不是那麼容易理解,所以讓我們一步一步來吧。

首先來看看Dart提供的產生新Isolate的函數:

  external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused = false,
      bool errorsAreFatal = true,
      SendPort? onExit,
      SendPort? onError,
      @Since("2.3") String? debugName});

這裡必填的只有兩個參數,entryPoint就相當於是new Isolate的main,而message就是main會收到的參數。

Hello Isolate

首先讓我們試著建立一個Isolate,並傳遞一則訊息:

void main() async {
  final newIsolate = await Isolate.spawn(newIsolateMain, "Hello I'm main Isolate!");
}

void newIsolateMain(String message) {
  print("new Isolate received: $message");
}

我們呼叫Isolate.spawn,傳入newIsolateMain和message,新的Isolate就會從newIsolateMaind開始執行,並收到message作為參數。這裡唯一要注意的是,newIsolateMain必須是global或static function。

接收Isolate訊息

接下來如果我想從new Isolate得到回覆呢?事情馬上就開始有趣起來了:

void main() async {
  final mainReceivePort = ReceivePort();
  final newIsolate = await Isolate.spawn(newIsolateMain, mainReceivePort.sendPort);
  final message = await mainReceivePort.first;
  print("main Isolate received: $message");
}

void newIsolateMain(SendPort mainSendPort) {
  mainSendPort.send("Hello from new Isolate");
}

Isolate的message是在port之間傳遞的,有點類似socket的概念。我們可以在Isolate中建立任意數量的ReceivePort,用來接收來自其它Isolate的訊息。每一個ReceivePort都會自帶一個SendPort,可以傳遞給其它Isolate,讓對方發送訊息回來。怎麼把我的SendPort傳遞給對方呢?當然是靠對方給我的SendPort來傳遞我的SendPort...
https://ithelp.ithome.com.tw/upload/images/20200919/20129053SRM5kTZPov.jpg
的確,一開始main Isolate根本沒有new Isolate的SendPort,我們唯一的機會就是把main的SendPort在Isolate.spawn時傳遞給對方。錯過了這個機會,未來就再也無法建立任何通訊了
把SendPort傳入newIsolateMain之後,new Isolate就可以傳遞訊息回來。這時候在main裡面就可以從mainReceivePort取回message。這裡的mainReceivePort實作了Stream,所以我們可以用first, listen等各種方式來處理取回的message。

實現雙向溝通

不過,如果我們最初傳遞參數給new Isolate的機會被SendPort用掉了,那我們該如何傳遞真正執行函數所需的參數呢?例如我們想讓new Isolate幫我們計算n+1,該如何把這個n傳過去?這下事情就變得非常有趣了

import 'dart:async';
import 'dart:isolate';

void main() async {
  // 1. 建立ReceivePort
  final mainPort = ReceivePort();
  // 2. 傳遞sendPort給new Isolate
  final newIsolate = await Isolate.spawn(newIsolateMain, mainPort.sendPort);

  mainPort.listen((message) {
    // 5. main Isolate接收到new Isolate的sendPort
    if (message is SendPort) { 
      int n = 42;
      // 6. 傳遞slowPlusOne所需的參數給new Isolate
      message.send(n); 
      print("main Isolate: Message sent to new Isolate: $n");
    // 10. 收到來自new Isolate的計算結果
    } else { 
      print("main Isolate: Received message from new Isolate: $message");
    }
  });
}

void newIsolateMain(SendPort mainSendPort) {
  // 3. new Isolate同樣建立ReceivePort
  final newPort = ReceivePort();
  // 4. 將sendPort回傳給main Isolate
  mainSendPort.send(newPort.sendPort);
  newPort.listen((message) async {
    print("new Isolate: Received message from main Isolate: $message");
    // 7. new Isolate接收到slowPlusOne的參數
    if (message is int) {
      // 8. 執行slowPlusOne
      final value = await slowPlusOne(message);
      // 9. 將結果回傳給main Isolate
      mainSendPort.send(value); 
      print("new Isolate: Message sent to main Isolate: $value");
    }
  });
}

Future<int> slowPlusOne(int n) => Future.delayed(Duration(seconds: 5), () => n+1);

總之,為了達成雙向溝通,我們必須先把main的sendPort傳給new,再把new的sendPort傳回來,就像是在進行hand-shake一樣。完成之後才能開始正常的傳遞參數,呼叫函數計算後回傳結果。不過,這時候我們是直接在new Isolate裡面固定呼叫slowPlusOne函數,如果我們也想把函數當作參數傳遞給new Isolate呢?

建立背景執行函數

讓我們來建立一個可以呼叫任何函數的runInBackground(function, argument)吧。到這裡其實已經沒有關於Isolate的新鮮事了,只是稍微再加入一點functional的觀念而已,想挑戰的人也可以自己試著實作看看:

import 'dart:async';
import 'dart:isolate';

void main() async {
  final result = await runInBackground(slowPlusOne, 42);
  print(result);
}

typedef OneArgumentFunction<T, R> = Future<R> Function(T message);

Future<R> runInBackground<T, R>(OneArgumentFunction<T, R> function, T argument) async {
  final mainPort = ReceivePort();
  final newIsolate = await Isolate.spawn(newIsolateMain, mainPort.sendPort);
  // 0. 這讓我們可以重複 await broadcast.first
  final broadcast = mainPort.asBroadcastStream();
  // 1. 接收來自new Isolate的SendPort
  final newSendPort = await broadcast.first as SendPort;
  // 2. 將function和argument一起傳遞給new Isolate
  newSendPort.send([function, argument]);
  // 6. 接收來自new Isolate的function執行結果
  final result = await broadcast.first as R;
  return result;
}

void newIsolateMain(SendPort mainSendPort) {
  final newPort = ReceivePort();
  mainSendPort.send(newPort.sendPort);
  newPort.listen((message) async {
    // 3. 接收來自main Isolate的function/argument
    final function = message[0];
    final argument = message[1];
    // 4. 執行並等待結果
    final result = await function(argument);
    // 5. 將結果回傳
    mainSendPort.send(result);
  });
}

Future<int> slowPlusOne(int n) => Future.delayed(Duration(seconds: 5), () => n+1);

這裡唯一須要注意的就是mainPort.asBroadcastStream()。因為我們會從mainPort收到兩個訊息,new Isolate的SendPort和函數執行結果。如果我們使用mainPort.listen(callback style),就沒辦法把結果從runInBackground再傳回main,我們必須使用await mainPort.first(sequential style)來取得結果才能再回傳。但因為呼叫first取得結果後stream就會被關閉,因此我們必須將它轉成broadcastStream來避免這個問題。

結語

完成了!恭喜你...重新發明了Dart提供的compute!其實我們也可以這樣:

import 'package:flutter/foundation.dart';

void main() async {
  final result = await compute(slowPlusOne, 42);
  print(result);
}

當然compute的實作比我們的還要更複雜一點,但概念大致上都是一樣的。雖然大多數簡單的背景執行需求都可以靠compute來達成,但理解了它背後的這些Isolate相關細節之後,未來你也可以依照自己的需來實作所需的compute函數,實現更複雜的平行處理。


上一篇
days[17] = "為什麼你應該嘗試從Provider升級到Riverpod?(下)"
下一篇
days[19] = "Event Loop是怎麼運作的?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
WenYeh
iT邦新手 4 級 ‧ 2021-04-04 21:28:39

Hello,請問一下,compute 和 isolate 最大的差別是什麼呀...,想問一下正常大型專案再處理背景處理時,會使用 compute 還是 isolate 比較多啊?

Joshua iT邦新手 4 級 ‧ 2021-04-06 23:03:50 檢舉

Isolate是Dart語言本身的多執行緒/異步處理機制,直接使用它的API比較複雜。compute是建立在Isolate之上的Dart內建函數,使用上比isolate的spawn/send message簡單很多。一般沒有特殊需求的話用compute就可以了,這篇主要是詳細說明compute和其背後的isolate如何運作。

WenYeh iT邦新手 4 級 ‧ 2021-04-08 16:29:44 檢舉

okok,了解,你的文章真的寫得很好,使我的 flutter 更上一層樓。謝謝~

我要留言

立即登入留言